#ifndef lua_controller_h
#define lua_controller_h

extern "C" {
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
}

#include "logger.h"
#include <exception>
#include <sstream>
#include <iostream>
#include <string>
#include <vector>
#include <map>
using namespace std;

/**
 * Exception used to inform of an error during the cast of a value on the
 * Lua stack.
 */
class TypeCastException: public exception {
  public:
    TypeCastException(const int p_index, const char* p_varName, const string& p_typeName) :
      index(p_index), varName(p_varName), typeName(p_typeName) {}
    virtual ~TypeCastException () throw() {}
    virtual const char* what () const throw () {
      stringstream ss;
      if (varName != NULL) {
        ss << "cannot cast variable: \"" << varName << "\" into: \"";
        ss << typeName << "\"";
      }
      else if (index > 0) {
        ss << "cannot cast object on the Lua stack at: " << index;
        ss << " into: \"" << typeName << "\"";
      }
      else {
        ss << "cannot get exception details.";
      }
      string error = ss.str();
      cerr << "TypeCastException thrown : " << error << endl;
      LOG_ERROR(error);
      return error.c_str();
    }
  private:
    int index;
    const char* varName;
    string typeName;
};

/**
  * Object able to communicate with a Lua script.
  */
class LuaController {
  public:
    inline LuaController ();
    inline ~LuaController () ;

    inline lua_State* getState ();

    /**
      * Loads a lua script into the current state.
      * Note that it is possible to load multiple scripts.
      * @throw an exception if the script cannot be loaded.
      */
    void loadScript (const string& script) throw(string);
    /**
      * Read a boolean from the specified variable name, or
      * at the top of the stack if NULL.
      * If the variable is not a boolean, throw a TypeCastException.
      */
    inline bool getBool(const char* varName=NULL)
    throw(TypeCastException, string);
    /**
      * Read an integer from the specified variable name, or
      * at the top of the stack if NULL.
      * If the variable is not an integer, throw a TypeCastException.
      */
    inline int getInt(const char* varName=NULL)
    throw(TypeCastException, string);
    /**
      * Read a double from the specified variable name, or
      * at the top of the stack if NULL.
      * If the variable is not a double, throw a TypeCastException.
      */
    inline double getDouble(const char* varName=NULL)
    throw(TypeCastException, string);
    /**
      * Read a string from the specified variable name, or
      * at the top of the stack if NULL.
      * If the variable is not a string or not convertible to a string, throw a TypeCastException.
      */
    inline string getString(const char* varName=NULL)
    throw(TypeCastException, string);
    /**
      * Reads a string->string table from the specified variable name,
      * or at the top of the stack if NULL.
      * If the variable is not a table, throw a TypeCastException.
      */
    template<class T>
    inline void getStringTable(map<string,string,T>& table,
                               const char* tableName=NULL)
    throw(TypeCastException, string);
    /**
      * Reads a string->int table from the specified variable name,
      * or at the top of the stack if NULL.
      */
    inline void getIntTable(map<string, int>& table, const char* tableName=NULL)
    throw(TypeCastException, string);
    /**
      * Reads a string->double table from the specified variable name,
      * or at the top of the stack if NULL.
      */
    inline void getDoubleTable(map<string, double>& table, const char* tableName=NULL)
    throw(TypeCastException, string);

    /**
     * Reads a table containing a set of tables to encode a 2d array.
     * If the variable is not a table, throw a TypeCastException. If the
     * subelements of the table are not a table itself, throw also this
     * exception.
     */
    void getString2DArray (vector< vector<string> >& array2d, 
                           const char* tableName=NULL)
    throw(TypeCastException, string);

  private:
    /**
      * Verify that the specified variable can be found on the Lua stack, or, if
      * NULL, that the stack is not empty so that the top value can be fetched.
      * On success, set the 'index' class member to the index of the value to be
      * read.
      *
      * On failure, throws a char* exception containing the error message.
      */
    inline void checkVariableAndStack (const char* varName) throw(string);

  private:
    lua_State* L;
    int index;
};

LuaController::
LuaController () : index(0) {
  L = luaL_newstate();
  if (L == NULL) {
    LOG_FATAL("Unable to create a new Lua state.");
  }
  else {
    LOG("New Lua state created.");
  }
  luaL_openlibs(L);
}

LuaController::
~LuaController () {
  // TODO : free state memory ?
}

lua_State* LuaController::
getState () {
  return L;
}

void LuaController::
loadScript (const string& script) throw(string) {
  stringstream ss;

  if (luaL_dofile(L, script.c_str())) {
    ss << "Unable to open script file : " << script << endl;
    ss << "Lua reported : " << lua_tostring(L, -1);
    throw ss.str();
  }
  else {
    ss << "Script opened : " << script;
    LOG(ss.str());
  }
}

void LuaController::
checkVariableAndStack (const char* varName) throw(string) {
  if (varName == NULL) {
    index = lua_gettop(L);
    if (index <= 0) {
      throw "Lua stack is empty.";
    }
    return;
  }
  lua_getglobal(L, varName);
  index = lua_gettop(L);
}

bool LuaController::
getBool (const char* varName) throw(TypeCastException, string) {
  checkVariableAndStack(varName);

  // Not a boolean
  if (lua_isboolean(L,index) == 0) {
    throw TypeCastException(index, varName, "boolean");
  }
  bool val = lua_toboolean(L,index);
  lua_pop(L,1);
  return val;
}

int LuaController::
getInt (const char* varName) throw(TypeCastException, string) {
  checkVariableAndStack(varName);

  // Not a number
  if (lua_isnumber(L,index) == 0) {
    throw TypeCastException(index, varName, "integer");
  }
  int val = lua_tointeger(L,index);
  lua_pop(L,1);
  return val;
}

double LuaController::
getDouble (const char* varName) throw(TypeCastException, string) {
  checkVariableAndStack(varName);

  // Not a number
  if (lua_isnumber(L,index) == 0) {
    throw TypeCastException(index, varName, "double");
  }
  double val = static_cast<double>(lua_tonumber(L,index));
  lua_pop(L,1);
  return val;
}

string LuaController::
getString (const char* varName) throw(TypeCastException, string) {
  checkVariableAndStack(varName);

  // Not a string
  if (lua_isstring(L,index) == 0) {
    throw TypeCastException(index, varName, "string");
  }
  string val = lua_tostring(L,index);
  lua_pop(L,1);
  return val;
}


/**
 * Reads a string->string table from the specified variable name,
 * or at the top of the stack if NULL.
 */
template<class T>
void LuaController::getStringTable(map<string,string,T>& table,
                                   const char* tableName) throw(TypeCastException, string) {
  checkVariableAndStack(tableName);

  // Not a table
  if (lua_istable(L,index) == 0) {
    throw TypeCastException(index, tableName, "table");
  }
  // Read the table
  lua_pushnil(L);
  while (lua_next(L,-2) != 0) {
    // Crash guard in the case the key is not directly a string but
    // a type implicitely convertible to a string.
    lua_pushvalue(L,-2);
    string key = lua_tostring(L, -1);
    string val = lua_tostring(L, -2);
    table[key] = val;
    lua_pop(L,2);
  }
}

/**
 * Reads a string->int table from the specified variable name,
 * or at the top of the stack if NULL.
 */
void getIntTable(map<string, int>& table, const char* tableName=NULL)
  throw(TypeCastException, string) {
    checkVariableAndStack(tableName);

    // Not a table
    if (lua_istable(L,index) == 0) {
      throw TypeCastException(index, tableName, "table");
    }
    // Read the table
    lua_pushnil(L);
    while (lua_next(L,-2) != 0) {
      // Crash guard in the case the key is not directly a string but
      // a type implicitely convertible to a string.
      lua_pushvalue(L,-2);
      string key = lua_tostring(L, -1);
      string val = lua_tointeger(L, -2);
      table[key] = val;
      lua_pop(L,2);
    }
  }

/**
 * Reads a string->double table from the specified variable name,
 * or at the top of the stack if NULL.
 */
void getDoubleTable(map<string, double>& table, const char* tableName=NULL)
  throw(TypeCastException, string) {
    checkVariableAndStack(tableName);

    // Not a table
    if (lua_istable(L,index) == 0) {
      throw TypeCastException(index, tableName, "table");
    }
    // Read the table
    lua_pushnil(L);
    while (lua_next(L,-2) != 0) {
      // Crash guard in the case the key is not directly a string but
      // a type implicitely convertible to a string.
      lua_pushvalue(L,-2);
      string key = lua_tostring(L, -1);
      double val = static_cast<double>lua_tonumber(L, -2);
      table[key] = val;
      lua_pop(L,2);
    }
  }

/**
  * Compare 2 strings containing numbers numerically and not lexicographically.
  */
class KeyCompare {
   public:
      bool operator() (const string x, const string y) {
        std::istringstream is_x(x);
        std::istringstream is_y(y);
        int xi;
        int yi;
        is_x >> xi;
        is_y >> yi;
        return xi <= yi;
      };
};

/**
  * Reads a table containing a set of tables to encode a 2d array.
  */
void LuaController::getString2DArray (vector< vector<string> >& array2d,
                                      const char* tableName) throw(TypeCastException, string) {

  checkVariableAndStack(tableName);

  // Not a table
  if (lua_istable(L,index) == 0) {
    throw TypeCastException(index, tableName, "table");
  }
  // Read the table
  lua_pushnil(L);
  while (lua_next(L,-2) != 0) {
    // Copy the key as an integer because it will be modified by the implicit
    // conversion of lua_tostring.
    lua_pushvalue(L, -2);
    // Push the value (a table) at the top of the stack to call getStringTable on the
    // table.
    lua_pushvalue(L, -2);
    // cout << "Type of val : " << (lua_type(L,-1) == LUA_TTABLE ? "Table" : "Not a table") << endl;
    /** The value is at -1 and it is a reference to a table,
      * so we can read it with our method, which will also do
      * all the necessary checks.
      * Note that we use the numerical comparator and not lexicographic compare.
      */
    map<string,string,KeyCompare> table; 
    getStringTable<KeyCompare>(table, NULL);
    /**
      * Here we simply get the list of values.
      */
    vector<string> val;
    map<string,string,KeyCompare>::iterator itr;
    for (itr = table.begin(); itr != table.end(); ++itr) {
      val.push_back ( (*itr).second );
    }
    
    array2d.push_back(val);
    lua_pop(L,3);
  }
}

#endif

